Completed
Push — master ( 53e70c...cba987 )
by Jan
15s queued 12s
created

index.js ➔ write   A

Complexity

Conditions 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 2
1
const fs = require('fs')
2
const ID3Definitions = require("./src/ID3Definitions")
3
const ID3Util = require('./src/ID3Util')
4
const ID3Helpers = require('./src/ID3Helpers')
5
const { isFunction, isString } = require('./src/util')
6
7
/*
8
**  Used specification: http://id3.org/id3v2.3.0
9
*/
10
11
/**
12
 * Checks and removes already written ID3-Frames from a buffer
13
 * @param {Buffer} data
14
 * @returns {boolean|Buffer}
15
 */
16
function removeTagsFromBuffer(data) {
17
    const framePosition = ID3Util.getFramePosition(data)
18
19
    if (framePosition === -1) {
20
        return data
21
    }
22
23
    const encodedSize = data.slice(framePosition + 6, framePosition + 10)
24
    if (!ID3Util.isValidEncodedSize(encodedSize)) {
25
        return false
26
    }
27
28
    if (data.length >= framePosition + 10) {
29
        const size = ID3Util.decodeSize(encodedSize)
30
        return Buffer.concat([
31
            data.slice(0, framePosition),
32
            data.slice(framePosition + size + 10)
33
        ])
34
    }
35
36
    return data
37
}
38
39
function writeInBuffer(tags, buffer) {
40
    buffer = removeTagsFromBuffer(buffer) || buffer
41
    return Buffer.concat([tags, buffer])
42
}
43
44
function writeAsync(tags, filebuffer, fn) {
45
    if(isString(filebuffer)) {
46
        try {
47
            fs.readFile(filebuffer, (error, data) => {
48
                if(error) {
49
                    fn(error)
50
                    return
51
                }
52
                const newData = writeInBuffer(tags, data)
53
                fs.writeFile(filebuffer, newData, 'binary', (error) => {
54
                    fn(error)
55
                })
56
            })
57
        } catch(error) {
58
            fn(error)
59
        }
60
    } else {
61
        fn(null, writeInBuffer(tags, filebuffer))
62
    }
63
}
64
65
function writeSync(tags, filebuffer) {
66
    if(isString(filebuffer)) {
67
        try {
68
            let data = fs.readFileSync(filebuffer)
69
            const newData = writeInBuffer(tags, data)
70
            fs.writeFileSync(filebuffer, newData, 'binary')
71
            return true
72
        } catch(error) {
73
            return error
74
        }
75
    }
76
77
    return writeInBuffer(tags, filebuffer)
78
}
79
80
/**
81
 * Write passed tags to a file/buffer
82
 * @param tags - Object containing tags to be written
83
 * @param filebuffer - Can contain a filepath string or buffer
84
 * @param fn - (optional) Function for async version
85
 * @returns {boolean|Buffer|Error}
86
 */
87
function write(tags, filebuffer, fn) {
88
    const completeTags = create(tags)
89
90
    if(isFunction(fn)) {
91
        return writeAsync(completeTags, filebuffer, fn)
92
    }
93
    return writeSync(completeTags, filebuffer)
94
}
95
96
/**
97
 * Creates a buffer containing the ID3 Tag
98
 * @param tags - Object containing tags to be written
99
 * @param fn fn - (optional) Function for async version
100
 * @returns {Buffer}
101
 */
102
function create(tags, fn) {
103
    const frames = ID3Helpers.createBufferFromTags(tags)
104
105
    //  Create ID3 header
106
    const header = Buffer.alloc(10)
107
    header.fill(0)
108
    header.write("ID3", 0)              //File identifier
109
    header.writeUInt16BE(0x0300, 3)     //Version 2.3.0  --  03 00
110
    header.writeUInt16BE(0x0000, 5)     //Flags 00
111
    ID3Util.encodeSize(frames.length).copy(header, 6)
112
113
    const id3Data = Buffer.concat([header, frames])
114
115
    if(isFunction(fn)) {
116
        fn(id3Data)
117
        return undefined
118
    }
119
    return id3Data
120
}
121
122
function readSync(filebuffer, options) {
123
    if(isString(filebuffer)) {
124
        filebuffer = fs.readFileSync(filebuffer)
125
    }
126
    return ID3Helpers.getTagsFromBuffer(filebuffer, options)
127
}
128
129
function readAsync(filebuffer, options, fn) {
130
    if(isString(filebuffer)) {
131
        fs.readFile(filebuffer, (error, data) => {
132
            if(error) {
133
                fn(error, null)
134
            } else {
135
                fn(null, ID3Helpers.getTagsFromBuffer(data, options))
136
            }
137
        })
138
    } else {
139
        fn(null, ID3Helpers.getTagsFromBuffer(filebuffer, options))
140
    }
141
}
142
143
/**
144
 * Read ID3-Tags from passed buffer/filepath
145
 * @param filebuffer - Can contain a filepath string or buffer
146
 * @param options - (optional) Object containing options
147
 * @param fn - (optional) Function for async version
148
 * @returns {boolean}
149
 */
150
function read(filebuffer, options, fn) {
151
    if(!options || typeof options === 'function') {
152
        fn = fn || options
153
        options = {}
154
    }
155
    if(isFunction(fn)) {
156
        return readAsync(filebuffer, options, fn)
157
    }
158
    return readSync(filebuffer, options)
159
}
160
161
/**
162
 * Update ID3-Tags from passed buffer/filepath
163
 * @param tags - Object containing tags to be written
164
 * @param filebuffer - Can contain a filepath string or buffer
165
 * @param options - (optional) Object containing options
166
 * @param fn - (optional) Function for async version
167
 * @returns {boolean|Buffer|Error}
168
 */
169
function update(tags, filebuffer, options, fn) {
170
    if(!options || typeof options === 'function') {
171
        fn = fn || options
172
        options = {}
173
    }
174
175
    const rawTags = Object.keys(tags).reduce((acc, val) => {
176
        if(ID3Definitions.FRAME_IDENTIFIERS.v3[val] !== undefined) {
177
            acc[ID3Definitions.FRAME_IDENTIFIERS.v3[val]] = tags[val]
178
        } else {
179
            acc[val] = tags[val]
180
        }
181
        return acc
182
    }, {})
183
184
    const updateFn = (currentTags) => {
185
        currentTags = currentTags.raw || {}
186
        Object.keys(rawTags).map((specName) => {
187
            const options = ID3Util.getSpecOptions(specName, 3)
188
            const cCompare = {}
189
            if(options.multiple && currentTags[specName] && rawTags[specName]) {
190
                if(options.updateCompareKey) {
191
                    currentTags[specName].forEach((cTag, index) => {
192
                        cCompare[cTag[options.updateCompareKey]] = index
193
                    })
194
195
                }
196
                if (!(rawTags[specName] instanceof Array)) {
197
                    rawTags[specName] = [rawTags[specName]]
198
                }
199
                rawTags[specName].forEach((rTag) => {
200
                    const comparison = cCompare[rTag[options.updateCompareKey]]
201
                    if (comparison !== undefined) {
202
                        currentTags[specName][comparison] = rTag
203
                    } else {
204
                        currentTags[specName].push(rTag)
205
                    }
206
                })
207
            } else {
208
                currentTags[specName] = rawTags[specName]
209
            }
210
        })
211
212
        return currentTags
213
    }
214
215
    if(!isFunction(fn)) {
216
        return write(updateFn(read(filebuffer, options)), filebuffer)
217
    }
218
219
    write(updateFn(read(filebuffer, options)), filebuffer, fn)
220
}
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
221
222
/**
223
 * @param {string} filepath - Filepath to file
224
 * @returns {boolean|Error}
225
 */
226
function removeTagsSync(filepath) {
227
    let data
228
    try {
229
        data = fs.readFileSync(filepath)
230
    } catch(error) {
231
        return error
232
    }
233
234
    const newData = removeTagsFromBuffer(data)
235
    if(!newData) {
236
        return false
237
    }
238
239
    try {
240
        fs.writeFileSync(filepath, newData, 'binary')
241
    } catch(error) {
242
        return error
243
    }
244
245
    return true
246
}
247
248
/**
249
 * @param {string} filepath - Filepath to file
250
 * @param {(error: Error) => void} fn - Function for async usage
251
 * @returns {void}
252
 */
253
function removeTagsAsync(filepath, fn) {
254
    fs.readFile(filepath, (error, data) => {
255
        if(error) {
256
            fn(error)
257
            return
258
        }
259
260
        const newData = removeTagsFromBuffer(data)
261
        if(!newData) {
262
            fn(error)
263
            return
264
        }
265
266
        fs.writeFile(filepath, newData, 'binary', (error) => {
267
            if(error) {
268
                fn(error)
269
            } else {
270
                fn(null)
271
            }
272
        })
273
    })
274
}
275
276
/**
277
 * Checks and removes already written ID3-Frames from a file
278
 * @param {string} filepath - Filepath to file
279
 * @param fn - (optional) Function for async usage
280
 * @returns {boolean|Error}
281
 */
282
function removeTags(filepath, fn) {
283
    if(isFunction(fn)) {
284
        return removeTagsAsync(filepath, fn)
285
    }
286
    return removeTagsSync(filepath)
287
}
288
289
function makeSwapParameters(fn) {
290
    return (a, b) => fn(b, a)
291
}
292
293
// The reorderParameter is a workaround because the callback function
294
// does not have a consistent interface between all the API functions.
295
// Ideally, all the functions should align with the promise style and
296
// always have the result first and the error second.
297
// Changing this would break the current public API.
298
// This could be changed internally and swap the parameter in a light
299
// wrapper when creating the public interface and then remove in a
300
// version 1.0 later with an API breaking change.
301
function makePromise(
302
    fn,
303
    reorderParameters = fn => (a, b) => fn(a, b)
304
) {
305
    return new Promise((resolve, reject) => {
306
        fn(reorderParameters((error, result) => {
307
            if(error) {
308
                reject(error)
309
            } else {
310
                resolve(result)
311
            }
312
        }))
313
    })
314
}
315
316
const PromiseExport = {
317
    create: (tags) => makePromise(create.bind(null, tags), makeSwapParameters),
0 ignored issues
show
Unused Code introduced by
The call to bind does not seem necessary since the function create declared on line 102 does not use this.
Loading history...
318
    write: (tags, file) => makePromise(write.bind(null, tags, file)),
0 ignored issues
show
Unused Code introduced by
The call to bind does not seem necessary since the function write declared on line 87 does not use this.
Loading history...
319
    update: (tags, file, options) => makePromise(update.bind(null, tags, file, options)),
0 ignored issues
show
Unused Code introduced by
The call to bind does not seem necessary since the function update declared on line 169 does not use this.
Loading history...
320
    read: (file, options) => makePromise(read.bind(null, file, options)),
0 ignored issues
show
Unused Code introduced by
The call to bind does not seem necessary since the function read declared on line 150 does not use this.
Loading history...
321
    removeTags: (filepath) => makePromise(removeTags.bind(null, filepath))
0 ignored issues
show
Unused Code introduced by
The call to bind does not seem necessary since the function removeTags declared on line 282 does not use this.
Loading history...
322
}
323
324
module.exports = {
325
    Constants: ID3Definitions.Constants,
326
    create,
327
    write,
328
    update,
329
    read,
330
    removeTags,
331
    removeTagsFromBuffer,
332
    Promise: PromiseExport
333
}
334